package adjust

import (
	"context"
	"sync"

	bytedtracer "code.byted.org/bytedtrace/interface-go"
	v1 "code.byted.org/bytertc/whiteboard_server/biz/api/pb/v1"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/biz_error"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/trace"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action"
	"code.byted.org/bytertc/whiteboard_server/biz/service/action/base"
	"code.byted.org/bytertc/whiteboard_server/biz/service/object"
	"code.byted.org/gopkg/gopool"
	"code.byted.org/gopkg/logs/v2/log"
	"github.com/pkg/errors"
	"github.com/spf13/cast"
	"google.golang.org/protobuf/proto"
)

func NewAction(ctx context.Context, baseAct base.Action, rawActions []*v1.Action) (actSet action.Action, err error) {
	switch rawActions[0].Type {
	case v1.ActionType_kTransform:
		set := &transformActionSet{
			actList: make([]*transformAction, 0),
		}
		for _, raw := range rawActions {
			transformDetail := &v1.TransformAction{}
			err = proto.Unmarshal(raw.Data, transformDetail)
			if err != nil {
				return nil, biz_error.DecodeError.Wrap(errors.WithStack(err))
			}
			act := &transformAction{
				Action:         baseAct,
				rawAction:      raw,
				transformDatas: transformDetail.UserData,
			}
			set.actList = append(set.actList, act)
		}
		actSet = set
	default:
		return nil, biz_error.DecodeError.Wrap(errors.Errorf("no such adjust action %d", rawActions[0].Type))
	}

	return actSet, nil
}

type transformActionSet struct {
	actList []*transformAction
	base.Action
}

func (ts *transformActionSet) Execute(ctx context.Context) error {
	var retErr error
	for _, act := range ts.actList {
		err := act.Execute(ctx)
		if err != nil {
			retErr = act.genError()
		}
	}
	return retErr
}
func (ts *transformActionSet) GetActionId() int32 {
	return 0
}

func (ts *transformActionSet) GetObjId() string {
	return ""
}
func (ts *transformActionSet) IsAtomic() bool {
	return false
}

func (ts *transformActionSet) RawActions() []*v1.Action {
	acts := make([]*v1.Action, 0)
	for _, act := range ts.actList {
		acts = append(acts, act.RawActions()...)
	}
	return acts

}

func (ts *transformActionSet) ConflictObjects() []string {
	var ret []string
	l := len(ts.actList)
	if l > 0 {
		ret = ts.actList[l-1].ConflictObjects()
	}
	return ret
}

type transformAction struct {
	base.Action
	rawAction      *v1.Action
	transformDatas map[string]*v1.TransformAction_UserData
	successObjs    sync.Map
}

func (t *transformAction) GetActionId() int32 {
	return t.rawAction.GetActionId()
}

func (t *transformAction) GetObjId() string {
	return ""
}

func (t *transformAction) ConflictObjects() []string {
	var objects []string
	for obj := range t.transformDatas {
		if _, exist := t.successObjs.Load(obj); !exist {
			objects = append(objects, obj)
		}
	}
	return objects
}
func (t *transformAction) GetOperator() string {
	return t.Operator
}

func (t *transformAction) IsAtomic() bool {
	return false
}

func (t *transformAction) genError() error {
	successCount := 0
	t.successObjs.Range(func(_, _ interface{}) bool {
		successCount++
		return true
	})
	if successCount != len(t.transformDatas) {
		return biz_error.Conflict.Wrap(errors.New(""))
	}
	return nil
}

func (t *transformAction) Execute(ctx context.Context) error {
	count := len(t.transformDatas)
	if count == 0 {
		return nil
	}
	objs := make([]string, 0, count)
	for obj := range t.transformDatas {
		objs = append(objs, obj)
	}
	objectsPerGoroutine := count / 10
	if objectsPerGoroutine < 5 {
		objectsPerGoroutine = 5
	}
	wg := sync.WaitGroup{}
	var start, end int
	for start = 0; start < len(objs); start = end {
		end = start + objectsPerGoroutine
		if end > len(objs) {
			end = len(objs)
		}
		st, en := start, end
		wg.Add(1)
		gopool.CtxGo(ctx, func() {
			defer wg.Done()
			iter := newTransformIter(t, objs[st:en])
			iter.execute(ctx)
		})
	}
	wg.Wait()
	return t.genError()
}

func (t *transformAction) RawActions() []*v1.Action {
	raw := &v1.TransformAction{
		UserData: make(map[string]*v1.TransformAction_UserData),
	}
	t.successObjs.Range(func(key, value interface{}) bool {
		objId := key.(string)
		raw.UserData[objId] = t.transformDatas[objId]
		return true
	})

	t.rawAction.Data, _ = proto.Marshal(raw)
	return []*v1.Action{t.rawAction}
}

func newTransformIter(t *transformAction, objIds []string) *transformActionIterator {
	return &transformActionIterator{
		transformAction: t,
		currentIndex:    -1,
		objIds:          objIds,
	}
}

type transformActionIterator struct {
	*transformAction
	currentIndex int
	objIds       []string
}

func (tIter *transformActionIterator) next() bool {
	tIter.currentIndex++
	return tIter.currentIndex < len(tIter.objIds)
}

func (tIter *transformActionIterator) GetObjId() string {
	return tIter.objIds[tIter.currentIndex]
}
func (tIter *transformActionIterator) GetActionId() int32 {
	if action, ok := tIter.transformDatas[tIter.GetObjId()]; ok {
		return action.GetObjectActionId()
	}
	return 0
}
func (tIter *transformActionIterator) execute(ctx context.Context) {
	span, ctx := bytedtracer.StartCustomSpan(ctx, trace.INTERNAL, trace.EditingSubErase)
	defer span.Finish()
	span.SetTag("object_id", tIter.objIds)

	for tIter.next() {
		err := tIter.transformObject(ctx)
		if err != nil {
			if !errors.Is(err, biz_error.Conflict) {
				log.V1.CtxError(ctx, "fail to transform object %s, %+v", tIter.GetObjId(), err)
			}
			continue
		}
		tIter.successObjs.Store(tIter.GetObjId(), struct{}{})
	}
}

func (tIter *transformActionIterator) transformObject(ctx context.Context) error {
	err := tIter.Repo.Authenticate(ctx, tIter)
	if err != nil {
		return err
	}
	ele, err := tIter.Repo.GetElement(ctx, tIter)
	if err != nil {
		return errors.WithMessagef(err, "element %s", tIter.GetObjId())
	}
	var transformData *v1.TransformAction_UserData
	var ok bool
	if transformData, ok = tIter.transformDatas[tIter.GetObjId()]; !ok {
		err = errors.Errorf("not contain objId:%s", tIter.GetObjId())
		log.V1.CtxError(ctx, "err:%+v", err)
		return err
	}
	if transformData.GetObjectActionId() != ele.GetActionId()+1 {
		err = errors.Errorf("actionId exception objId:%s,ele actid:%d,tran actid:%d", tIter.GetObjId(), ele.GetActionId(), transformData.GetObjectActionId())
		log.V1.CtxError(ctx, "err:%+v", err)
		return biz_error.Conflict.Wrap(err)
	}
	count, err := tIter.Repo.IncrTransformCount(ctx, tIter, tIter.GetOperator())
	//是否有问题？
	if err != nil {
		return err
	}
	//只有finish和kfull才会落库
	if tIter.rawAction.Phase != v1.PhaseType_kFinish && tIter.rawAction.Phase != v1.PhaseType_kFull {
		return nil
	}
	switch v := ele.(type) {
	case *object.Pen:
		v.Transform = transformData.GetTransform()
	case *object.DefaultElement:
		switch msg := v.ProtoObject.(type) {
		case *v1.Pen:
			msg.Transform = transformData.GetTransform()
		case *v1.Rect:
			msg.Transform = transformData.GetTransform()
		case *v1.Circle:
			msg.Transform = transformData.GetTransform()
		case *v1.Text:
			msg.Transform = transformData.GetTransform()
		case *v1.Image:
			msg.Transform = transformData.GetTransform()
		case *v1.Line:
			msg.Transform = transformData.GetTransform()
		case *v1.Arrow:
			msg.Transform = transformData.GetTransform()
		case *v1.Audio:
			msg.Transform = transformData.GetTransform()
		case *v1.Video:
			msg.Transform = transformData.GetTransform()
		}
	default:
		return nil
	}
	ele.AddActionCount(cast.ToInt32(count))
	ele.SetActionId(ele.GetActionId() + 1)
	return tIter.Repo.SaveElement(ctx, ele)
}
